/* Copyright (c) 2015,2016 Lucky Byte, Inc.
 */
package com.lucky_byte.pay.jar.p091;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.util.encoders.Hex;

import com.google.gson.JsonObject;
import com.lucky_byte.pay.jar.BCDUtils;
import com.lucky_byte.pay.jar.ByteUtils;
import com.lucky_byte.pay.jar.Field;
import com.lucky_byte.pay.jar.Packet;
import com.lucky_byte.pay.jar.Runtime;

/**
 * 009.1 报文
 */
public class Packet091 extends Packet
{
	private static final Logger logger = LogManager.getLogger();

	private static final String ATTRS_PATH =
			Runtime.pathFor("/etc/091.properties");
	private static final Map<String, Field.Attr> ATTRS =
			Field.attrsFromPath(ATTRS_PATH);

	// 拨号监控信息 LRI 首尾标识序列
	private static final byte[] LRI_HEAD = {
			0x4C, 0x52, 0x49, 0x00, 0x1C
	};
	private static final byte[] LRI_TAIL = {
			0x58, 0x00, 0x09, 0x49, 0x00, 0x06,
			0x00, 0x00, 0x00, 0x22, 0x00, 0x30
	};
	private byte[] in_bytes;
	private byte[] tpdu;
	private byte[] head;
	private byte[] dial_ani;
	private byte[] dial_dni;
	private Map<String, String> fset;

	public Packet091() {
		this.tpdu = new byte[5];
		this.head = new byte[6];
		this.fset = new HashMap<>();
	}

	@Override
	public boolean has(String name) {
		return this.fset.containsKey(name);
	}

	@Override
	public String get(String name) {
		return this.fset.get(name);
	}

	@Override
	public boolean set(String name, String value) {
		try {
			int no = Integer.parseInt(name);
			if (no < 1 || no > 64) {
				logger.error("字段名称[{}]超出范围[1-64].", name);
				return false;
			}
		} catch (NumberFormatException e) {
			logger.error("字段名称[{}]不是一个整数.", name);
			return false;
		}
		Field.Attr attrs = ATTRS.get(name);
		if (attrs == null) {
			logger.error("不能设置配置中不存在的字段[{}].", name);
			return false;
		}
		if (value == null) {
			this.fset.remove(name);
			return true;
		} else {
			this.fset.put(name, value);
		}
		// 检查设置的值是否符合字段配置的属性
		switch (attrs.fmter) {
		case Field.Attr.FMTER_ASC:
		case Field.Attr.FMTER_BCD:
			if (value.length() > attrs.maxlen) {
				logger.error("设置字段[{}]值[{}]的长度[{}]超出字段配置的最大长度[{}]",
						name, value, value.length(), attrs.maxlen);
				return false;
			}
			if (attrs.regex != null && !value.matches(attrs.regex)) {
				logger.error("设置字段[{}]的值[{}]不匹配[{}]，请检查.",
						name, value, attrs.regex);
				return false;
			}
			break;
		case Field.Attr.FMTER_HEX:
			if (value.length() % 2 != 0) {
				logger.error("设置 BIN/HEX 字段[{}]值[{}]的长度[{}]不是二的整倍数.",
						name, value, value.length());
				return false;
			}
			if (value.length() / 2 > attrs.maxlen) {
				logger.error("设置字段[{}]的值[{}][{}]超出字段配置的最大长度[{}].",
						name, value, value.length() / 2, attrs.maxlen);
				return false;
			}
			break;
		}
		return true;
	}

	@Override
	public int count() {
		return this.fset.size();
	}

	@Override
	public List<String> keySet() {
		List<String> keyset = new ArrayList<>();
		for (String key : fset.keySet()) {
			keyset.add(key);
		}
		return keyset;
	}

	@Override
	public void remove(String name) {
		this.fset.remove(name);
	}

	@Override
	public void clear() {
		this.fset.clear();
	}

	@Override
	public Packet clone() {
		Packet091 packet = new Packet091();

		for (String key : this.fset.keySet()) {
			packet.fset.put(key, this.fset.get(key));
		}
		packet.tpdu = this.tpdu.clone();
		packet.head = this.head.clone();

		if (this.dial_ani != null) {
			packet.dial_ani = this.dial_ani.clone();
		}
		if (this.dial_dni != null) {
			packet.dial_dni = this.dial_dni.clone();
		}
		return packet;
	}

	@Override
	public JsonObject toJson() {
		JsonObject json_object = new JsonObject();

		for (String key : fset.keySet()) {
			json_object.addProperty(key, fset.get(key));
		}
		if (this.tpdu != null) {
			json_object.addProperty("tpdu", this.getTpdu());
		}
		if (this.head != null) {
			json_object.addProperty("head", this.getHead());
		}
		return json_object;
	}

	@Override
	public boolean setTpdu(String tpdu) {
		if (tpdu == null || tpdu.length() != 10) {
			logger.error("TPDU 字符串的长度必须等于 10.");
			return false;
		}
		this.tpdu = Hex.decode(tpdu);
		return true;
	}

	@Override
	public String getTpdu() {
		if (this.tpdu == null) {
			return null;
		}
		return Hex.toHexString(this.tpdu);
	}

	@Override
	public boolean setHead(String head) {
		if (head == null || head.length() != 12) {
			logger.error("报文头字符串的长度必须等于 12.");
			return false;
		}
		this.head = Hex.decode(head);
		return true;
	}

	@Override
	public String getHead() {
		if (this.head == null) {
			return null;
		}
		return Hex.toHexString(this.head);
	}

	@Override
	public boolean setDialANI(String ani) {
		if (ani == null || ani.length() == 0) {
			this.dial_ani = null;
			return true;
		}
		if (ani.length() != 16) {
			logger.error("拨号主叫号码字符串的长度必须等于 16.");
			return false;
		}
		this.dial_ani = Hex.decode(ani);
		return true;
	}

	@Override
	public String getDialANI() {
		if (this.dial_ani == null) {
			return null;
		}
		return Hex.toHexString(this.dial_ani);
	}

	@Override
	public boolean setDialDNI(String dni) {
		if (dni == null || dni.length() == 0) {
			this.dial_dni = null;
			return true;
		}
		if (dni.length() != 16) {
			logger.error("拨号被叫号码字符串的长度必须等于 16.");
			return false;
		}
		this.dial_dni = Hex.decode(dni);
		return true;
	}

	@Override
	public String getDialDNI() {
		if (this.dial_dni == null) {
			return null;
		}
		return Hex.toHexString(this.dial_dni);
	}

	/**
	 * 获取用于解析的原始数据
	 */
	public byte[] inBytes() {
		return this.in_bytes;
	}

	/**
	 * 解析报文字节流
	 */
	public boolean readBytes(byte[] bytes, int size) {
		if (size <= 0) {
			size = bytes.length;
		}
		logger.info("开始解析报文，字节流长度[{}]", size);

		try {
			DataInputStream stream = new DataInputStream(
					new ByteArrayInputStream(bytes, 0, size));
			if (!this.readHead(stream)) {
				return false;
			}
			// 第一个域为交易类型
			if (!this.readField(stream, 1)) {
				return false;
			}
			// Bitmap 的第一位是扩展标识，表示后面使用 64 位还是 128 位的 Bitmap
			byte[] bitmap = new byte[32];
			stream.readFully(bitmap, 0, 1);
			int bitmap_bytes = (bitmap[0] & 0x80) != 0 ? 16 : 8;
			stream.readFully(bitmap, 1, bitmap_bytes - 1);

			logger.info("读取[BITMAP][{}]",
					ByteUtils.format(bitmap, 0, bitmap_bytes));
			if (bitmap_bytes == 16) {
				logger.warn("009.1 报文中 BITMAP 的位数应该为 64.");
			}
			// 第一位是扩展标志，不能用于确定字段是否存在
			bitmap[0] &= 0x7F;

			// 根据 BITMAP 逐个读取字段
			for (int i = 0; i < bitmap_bytes; i++) {
				for (int j = 0; j < 8; j++) {
					if ((bitmap[i] & (0x80 >> j)) != 0) {
						if (!this.readField(stream, i * 8 + j + 1)) {
							return false;
						}
					}
				}
			}
			logger.info("共读取[{}]个字段.", fset.size());

			// 报文所有字节应该读取完毕
			int remain = stream.available();
			if (remain > 0) {
				logger.warn("接收到的报文解析完后还剩余[{}]字节.", remain);
			}
			this.in_bytes = bytes;
			return true;
		} catch (EOFException e) {
			logger.error("报文内容不足以继续解析其 Bitmap 中描述的字段.");
			return false;
		} catch (Exception e) {
			logger.catching(e);
			return false;
		}
	}

	/**
	 * 读取 TPDU 以及报文头信息
	 */
	private boolean readHead(DataInputStream stream) throws IOException {
		stream.readFully(this.tpdu);
		if (this.tpdu[0] != 0x60) {
			logger.warn("报文头中应用类别[{}]不等于 60H，"
					+ "可能是 POS 终端采用非 009.1 通讯协议，请确认.",
					Hex.toHexString(tpdu, 0, 1));
			return false;
		}
		// 读取 5 个字节，看看是否匹配拨号监控 LRI 开始标识序列
		byte[] try_bytes = new byte[6];
		stream.readFully(try_bytes, 0, 5);

		// 尝试匹配 LRI 开始标识序列
		boolean lri_head_match = true;
		for (int i = 0; i < 5; i++) {
			if (try_bytes[i] != LRI_HEAD[i]) {
				lri_head_match = false;
				break;
			}
		}
		// 如果不匹配，则只需要再读取一个字节的报文头部即可.
		if (!lri_head_match) {
			stream.readFully(try_bytes, 5, 1);
			this.head = try_bytes;
			return true;
		}
		// 开始读拨号信息
		this.dial_ani = new byte[8];
		this.dial_dni = new byte[8];
		stream.readFully(this.dial_ani);
		stream.readFully(this.dial_dni);

		// 继续检查拨号监控信息的 LRI 结束标识序列是否匹配
		byte[] tail = new byte[12];
		stream.readFully(tail);
		for (int i = 0; i < 12; i++) {
			if (tail[i] != LRI_TAIL[i]) {
				logger.warn("报文中拨号监控信息结束标识序列第[{}]个字节不匹配，"
						+ "报文解析失败.", i + 1);
				return false;
			}
		}
		// 继续读取完整的报文头 6 字节
		stream.readFully(this.head);
		if (head[0] != 0x60) {
			logger.warn("报文头中应用类别[{}]不等于 60H，"
					+ "可能是 POS 终端采用非 009.1 通讯协议，请确认.",
					Hex.toHexString(head, 0, 1));
			return false;
		}
		return true;
	}

	/**
	 * 读取报文字段
	 */
	private boolean readField(DataInputStream stream, int no)
			throws IOException {
		String name = Integer.toString(no);
		Field.Attr attr = ATTRS.get(name);
		if (attr == null) {
			logger.error("文件[{}]中未找到字段[{}]的配置.", ATTRS_PATH, name);
			return false;
		}
		int len = attr.maxlen;
		if (attr.llvar > 0) {
			len = this.readFieldLLvar(stream, attr);
			if (len < 0) {
				logger.error("变长字段[{}]的长度前缀[{}]无效.", name, len);
				return false;
			}
		}
		if (len > attr.maxlen) {
			logger.error("字段[{}]的长度[{}]超出配置中最大长度[{}].",
					name, len, attr.maxlen);
			return false;
		}
		int size = len;
		if (attr.fmter == Field.Attr.FMTER_BCD) {
			size = (len + 1) / 2;
		}
		byte[] read_bytes = new byte[size];
		stream.readFully(read_bytes, 0, size);
		return this.addField(name, len, read_bytes, attr);
	}

	/**
	 * 添加字段
	 */
	private boolean addField(String name, int len, byte[] bytes,
			Field.Attr attr) throws UnsupportedEncodingException {
		String value;

		switch (attr.fmter) {
		case Field.Attr.FMTER_ASC:
			// ASC 数据采用 GB18030 编码
			value = new String(bytes, "GB18030");
			break;
		case Field.Attr.FMTER_BCD:
			// 展开 BCD 编码的数据
			// BCD 数据的长度有可能为奇数，因此需要去除多余的填充字符
			value = Hex.toHexString(bytes);
			if (value.length() > len) {
				if (attr.align == Field.Attr.ALIGN_LEFT) {
					value = value.substring(0, len);
				} else {
					value = value.substring(value.length() - len);
				}
			}
			break;
		case Field.Attr.FMTER_HEX:
			// 展开 HEX 编码的数据, HEX 编码读数据不存在填充字符
			value = Hex.toHexString(bytes);
			break;
		default:
			logger.error("字段[{}]配置的fmter[{}]无效.", name, attr.fmter_str);
			return false;
		}
		// 如果配置了正则表达式，也需要验证
		if (attr.regex != null) {
			if (attr.fmter == Field.Attr.FMTER_ASC ||
				attr.fmter == Field.Attr.FMTER_BCD) {
				if (!value.matches(attr.regex)) {
					logger.error("字段[{}]内容[{}]不匹配[{}]，请检查.",
							name, value, attr.regex);
					return false;
				}
			}
		}
		fset.put(name, value);

		// BIN 格式可能实际上是 ASC 编码的数据，这里进行测试，如果是则采用 ASC 来记录日志
		if (attr.fmter == Field.Attr.FMTER_HEX) {
			byte[] bs = Hex.decode(value);
			boolean printable = true;
			for (int i = 0; i < bs.length; i++) {
				if (bs[i] < 32 || bs[i] >= 127) {
					printable = false;
					break;
				}
			}
			if (printable) {
				String v = new String(bs);
				logger.info("读取报文字段[{}][{}][{}][{}]",
						name, v, v.length(), attr.fmter_str);
				return true;
			}
		}
		logger.info("读取报文字段[{}][{}][{}][{}]",
				name, value, value.length(), attr.fmter_str);
		return true;
	}

	/**
	 * 读取变长字段的长度前缀
	 * POS 009.1 变长字段的长度前缀统一采用 BCD 编码
	 */
	private int readFieldLLvar(DataInputStream stream,
			Field.Attr attrs) throws IOException {
		int llvar = (attrs.llvar + 1) / 2;
		byte[] llvar_bytes = new byte[llvar];
		stream.readFully(llvar_bytes, 0, llvar);
		return (int) BCDUtils.decode(llvar_bytes, -1);
	}

	/**
	 * 重组报文为字节流
	 */
	public byte[] packBytes() {
		return this.packBytes(true);
	}

	/**
	 * 重组报文为字节流
	 */
	public byte[] packBytes(boolean logit) {
		try {
			ByteArrayOutputStream stream = new ByteArrayOutputStream(1024);
			DataOutputStream data_os = new DataOutputStream(stream);

			this.packHead(stream, logit);

			// 消息类型在前
			if (!this.has("1")) {
				logger.error("报文缺少消息类型字段，重组报文字节流失败.");
				return null;
			}
			this.packField(data_os, "1", logit);

			// 添加 BITMAP 前，需要对报文字段进行排序
			SortedSet<String> sort_keys = new TreeSet<String>(
					new Comparator<String>() {
				public int compare(String s1, String s2) {
					int no1 = Integer.parseInt(s1);
					int no2 = Integer.parseInt(s2);
					return no1 - no2;
				}
			});
			sort_keys.addAll(fset.keySet());

			if (!packBitmap(sort_keys, stream, logit)) {
				return null;
			}
			int count = 1;
			for (String key : sort_keys) {
				if (key.equals("1")) {
					continue;
				}
				this.packField(data_os, key, logit);
				count++;
			}
			if (logit) {
				logger.info("共打包[{}]个字段.", count);
			}
			return stream.toByteArray();
		} catch (IOException e) {
			logger.error("重组报文字节流错误[{}].", e.getMessage());
			return null;
		}
	}

	/**
	 * 重组报文头
	 */
	private void packHead(OutputStream stream, boolean logit)
			throws IOException {
		if (logit) {
			logger.info("打包[TPDU][{}]", ByteUtils.format(this.tpdu));
		}
		stream.write(this.tpdu);
		if (this.dial_ani != null && this.dial_dni != null) {
			if (logit) {
				logger.info("打包拨号信息[{} | {} | {} | {}]",
						LRI_HEAD, this.dial_ani, this.dial_dni, LRI_TAIL);
			}
			stream.write(LRI_HEAD);
			stream.write(this.dial_ani, 0, 8);
			stream.write(this.dial_dni, 0, 8);
			stream.write(LRI_TAIL);
		}
		if (logit) {
			logger.info("打包报文头[{}]", ByteUtils.format(this.head));
		}
		stream.write(this.head);
	}

	/**
	 * 计算并输出 bitmap
	 * 根据不同的规范，bitmap 可能使用 64 位或 128 位
	 */
	private boolean packBitmap(SortedSet<String> sort_keys,
			OutputStream stream, boolean logit) throws IOException {
		byte[] bitmap = getBitmap(sort_keys);
		if (bitmap == null) {
			return false;
		}
		if (logit) {
			logger.info("打包[BITMAP][{}]", ByteUtils.format(bitmap).trim());
		}
		stream.write(bitmap, 0, bitmap.length);
		return true;
	}

	/**
	 * 根据当前报文中的字段生成 bitmap 字节流
	 *
	 * @return Bitmap 字节流, 或 null
	 */
	protected byte[] getBitmap(SortedSet<String> sort_keys) {
		int bitmap_bits = 64;
		byte[] bitmap = new byte[bitmap_bits / 8];

		// 通过报文中的字段编号设置正确的 bitmap 位
		for (String key : sort_keys) { 
			try {
				int no = Integer.parseInt(key);
				if (no == 1) {
					continue;
				}
				if (no < 1 || no > bitmap_bits) {
					logger.error("字段编号[{}]超出范围[1-{}].", no, bitmap_bits);
					return null;
				}
				no -= 1;
				bitmap[no / 8] |= (0x80 >> (no % 8));
			} catch (NumberFormatException e) {
				logger.error("字段[{}]的名称不是一个整数，重组报文失败", key);
				return null;
			}
		}
		return bitmap;
	}

	private byte[] paddingWithAttr(String name, byte[] bytes, Field.Attr attr) {
		// 非定长字段不需要补足
		if (attr.llvar > 0) {
			return bytes;
		}
		if (bytes.length > attr.maxlen) {
			logger.error("字段[{}]到内容长度[{}]超出了定义的最大长度[{}].",
					name, bytes.length, attr.maxlen);
			return ByteUtils.truncate(bytes, 0, attr.maxlen);
		}
		if (bytes.length == attr.maxlen) {
			return bytes;
		}
		byte[] padded_bytes = new byte[attr.maxlen];
		for (int i = 0; i < attr.maxlen; i++) {
			padded_bytes[i] = attr.padval;
		}
		if (attr.align == Field.Attr.ALIGN_LEFT) {
			System.arraycopy(bytes, 0, padded_bytes, 0, bytes.length);
		} else {
			System.arraycopy(bytes, 0, padded_bytes,
					attr.maxlen - bytes.length, bytes.length);
		}
		return padded_bytes;
	}

	/**
	 * 打包字段
	 */
	private void packField(DataOutputStream stream, String name, boolean logit)
			throws IOException {
		Field.Attr attr = ATTRS.get(name);
		String value = this.fset.get(name);
		String pack_value;

		// 对于变长字段，先输出字段长度部分
		if (attr.llvar > 0) {
			int lllen = (attr.llvar + 1) / 2 * 2;
			int write_len = value.length();
			// 如果为 HEX，则长度为字符串长度的一半
			if (attr.fmter == Field.Attr.FMTER_HEX) {
				write_len /= 2;
			}
			String len = String.format("%0" + lllen + "d", write_len);
			stream.write(BCDUtils.encode(len));
		}
		byte[] bytes = value.getBytes("GB18030");

		switch (attr.fmter) {
		case Field.Attr.FMTER_ASC:
			bytes = this.paddingWithAttr(name, bytes, attr);
			pack_value = new String(bytes, "GB18030");
			break;
		case Field.Attr.FMTER_BCD:
			// 先进行填补（如果需要的话)，如果补完是长度是奇数，还需要填充一个字节
			bytes = this.paddingWithAttr(name, bytes, attr);
			if (bytes.length % 2 != 0) {
				byte[] padval = new byte[] { attr.padval };
				if (attr.align == Field.Attr.ALIGN_LEFT) {
					bytes = ByteUtils.concat(bytes, padval);
				} else {
					bytes = ByteUtils.concat(padval, bytes);
				}
			}
			// 这里不能严格限制为 BCD 字符，因为二磁道、三磁道等虽然声明为 BCD 编码，
			// 实则包含非 BCD 字符，例如 b, d 等等.
			bytes = Hex.decode(bytes);
			pack_value = Hex.toHexString(bytes);
			break;
		case Field.Attr.FMTER_HEX:
			// 先转换为二进制后在进行填补，因为二进制的 maxlen 指的是字节长度
			bytes = Hex.decode(bytes);
			bytes = this.paddingWithAttr(name, bytes, attr);
			pack_value = Hex.toHexString(bytes);
			break;
		default:
			logger.error("文件[{}]中字段[{}]配置的fmter[{}]无效.",
					attr.path, name, attr.fmter_str);
			return;
		}
		stream.write(bytes);
		if (logit) {
			logger.info("打包字段[{}][{}][{}][{}]", name, pack_value,
					bytes.length, attr.fmter_str);
		}
	}
}
